Patch "360 Degree Movement" by "Skyth"
WriteNop(0x1404615B7, 4)

Code "Enhanced Air Rotation" in "Physics" by "Hyper" does "Allows for tighter mid-air control for more precise movements when jumping, more akin to Sonic Frontiers."
//
    #lib "DeltaTime"
    #lib "GameManager"
    #lib "GameObject"
    #lib "MathHelpers"
    #lib "Memory"
    #lib "Player"
    #lib "VectorHelpers"

    #import "GOComponents"
    #import "Input"
    #import "PlayerCollision"

    #load "System.Numerics.dll"

    using System.Collections.Generic;
    using System.Numerics;
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////
    //                                         MODIFIABLE PARAMETERS                                         //
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////
    
    // The default acceleration applied when moving in 3D.
    const float _accelerationFV = 3.25f;

    // The default acceleration applied when moving in 2D.
    const float _accelerationSV = 1.25f;

    // The threshold for detecting if the stick is held in the opposite direction to the player for deceleration.
    const float _axisDecelerationThreshold = 0.15f;

    // The deceleration rate when colliding with walls.
    const float _collisionDeceleration = 1.0f;

    // The rotation force for 3D.
    const float _rotationForceFV = 0.005f;

    // The rotation force when falling in 3D.
    const float _rotationForceFallFV = 0.025f;

    // The maximum rotation force for 2D.
    const float _rotationForceMaxSV = 0.25f;

    // The minimum rotation force for 2D.
    const float _rotationForceMinSV = 0.05f;

    // The states that have custom acceleration.
    static Dictionary<int, float> _accelTable = new()
    {
        { (int)Buddy.StateID.BuddyStateGadgetBurstFall, 1.75f }
    };

    // The states that are allowed physics changes.
    static List<int> _allowedStates = new()
    {
        (int)Buddy.StateID.BuddyStateGadgetBurstAir,
        (int)Buddy.StateID.BuddyStateGadgetBurstFall,
        (int)Sonic.StateID.StateDoubleJump,
        (int)Sonic.StateID.StateFollowDoubleJump,
        (int)Sonic.StateID.StateFollowJump,
        (int)Sonic.StateID.StateGrindJump,
        (int)Sonic.StateID.StateGrindDoubleJump,
        (int)Sonic.StateID.StateJump,
        (int)Sonic.StateID.StatePinBall,
        (int)Sonic.StateID.StateSpinAttack
    };

    // The states that are only allowed rotation changes.
    static List<int> _rotationOnlyStates = new()
    {
        (int)Sonic.StateID.StateBoostFall,
        (int)Sonic.StateID.StateFall,
        (int)Sonic.StateID.StateFollowFall,
        (int)Sonic.StateID.StateHomingFinished
    };

    // The states that are not allowed physics changes.
    static List<(int Previous, int Current)> _disallowedStatePairs = new()
    {
        ( (int)Buddy.StateID.BuddyStateWireTrapezeAuto, (int)Buddy.StateID.StateFall       ),
        ( (int)Buddy.StateID.BuddyStateWireTrapezePath, (int)Buddy.StateID.StateFall       ),
        ( (int)Sonic.StateID.StateCaught,               (int)Sonic.StateID.StateFall       ),
        ( (int)Sonic.StateID.StateFollowSpring,         (int)Sonic.StateID.StateFollowFall ),
        ( (int)Sonic.StateID.StateSpringJump,           (int)Sonic.StateID.StateFall       )
    };
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////
    // ! ! ! ! ! ! ! ! ! WARNING: THE CODE BEYOND THIS POINT IS HELD TOGETHER BY DUCT TAPE ! ! ! ! ! ! ! ! ! //
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////

    static bool _isInitialised = false;
    static bool _isInitialKinematicsCaptured = false;

    static Vector3 _initialVelocity = Vector3.Zero;

    static int _previousState = -1;
    static int _previousDisallowedState = -1;

    static bool IsStatePairAllowed(int in_currentState)
    {
        var result = true;

        foreach (var pair in _disallowedStatePairs)
        {
            if (_previousState == pair.Previous && in_currentState == pair.Current)
            {
                _previousDisallowedState = pair.Current;
                result = false;
                break;
            }
        }

        _previousState = in_currentState;

        return result;
    }

    static bool IsFalling(int in_currentState)
    {
        return (Sonic.StateID)in_currentState is
            Sonic.StateID.StateFall or
            Sonic.StateID.StateFollowFall or
            Sonic.StateID.StateBoostFall;
    }

    static float GetAcceleration(int in_currentState)
    {
        if (_accelTable.ContainsKey(in_currentState))
            return _accelTable[in_currentState];

        return _accelerationFV;
    }

    static void SetAxisDeadzone(Player.Data* in_pPlayer, float in_deadZone)
    {
        if (in_pPlayer == null)
            return;
        
        if (GameObject.TryGetGOC<GOCInput>(&in_pPlayer->Base, out GOCInput* out_pGOCInput))
            return;

        out_pGOCInput->pInputComponent->pInputMapSettings->SetAxisDeadzone(in_deadZone);
    }

    static void SetVelocityXZ(GOCPlayerKinematicParams* in_pGOCPlayerKinematicParams, Vector3 in_newVelocity)
    {
        /* Apply velocity only to the X and Z axes
           to allow Forces to use its own jump height. */
        in_pGOCPlayerKinematicParams->Velocity.X = in_newVelocity.X;
        in_pGOCPlayerKinematicParams->Velocity.Z = in_newVelocity.Z;
    }

    static bool IsDeceleratingOnCollision(Player.Data* in_pPlayer, GOCPlayerKinematicParams* in_pGOCPlayerKinematicParams)
    {
        if (in_pPlayer == null)
            return false;
        
        if (!GameObject.TryGetGOC(&in_pPlayer->Base, out GOCPlayerCollider* out_pGOCPlayerCollider))
            return false;

        var raycasts = out_pGOCPlayerCollider->pPlayerCollision->Raycasts;

        if (raycasts.Length <= 0)
            return false;
        
        var raycast = raycasts[0];

        // Ignore raycast if it's a floor.
        if (raycast.Normal.Y > 0.0f)
            return false;
        
        // Create quaternion from wall surface normal.
        var wallRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitY, (float)Math.Acos(Vector3.Dot(Vector3.UnitY, raycast.Normal)));

        // Get rotation of the player in the direction of the wall.
        var relativeRotation = in_pGOCPlayerKinematicParams->Rotation * Quaternion.Inverse(wallRotation);

        // Get angle cosine from W axis to calculate relative angle to the wall.
        var acos = MathHelpers.Clamp(Math.Acos(relativeRotation.W), 0.0f, 1.0f);

        /* Get angle in degrees and remap it, so facing away from
           the wall results in zero, then we can use it for lerping. */
        var degrees = MathHelpers.Remap(MathHelpers.ToDegrees(2.0f * (float)acos), 0.0f, 90.0f, 90.0f, 0.0f);
        
        // The current player velocity.
        var velocity = in_pGOCPlayerKinematicParams->Velocity;

        // The new velocity with deceleration applied by the angle of the player relative to the wall.
        var newVelocity = VectorHelpers.Lerp(velocity, velocity / _collisionDeceleration, degrees / 90.0f);

        /* Update only the X and Z axes of the
           initial velocity, so the player doesn't
           go flying once parallel with the wall. */
        _initialVelocity.X = newVelocity.X;
        _initialVelocity.Z = newVelocity.Z;

        // Update only the X and Z axes of the current velocity.
        SetVelocityXZ(in_pGOCPlayerKinematicParams, newVelocity);

        return true;
    }
//
{
    if (!_isInitialised)
    {
        // Disable jump rotation.
        Memory.WriteForceJump(0x1404462A3);

        _isInitialised = true;
    }

    var pPlayer = Player.GetPlayerData();

    if (pPlayer == null)
        return;

    var pContext = pPlayer->GetContext();

    if (pContext == null)
        return;
    
    if (!GameObject.TryGetGOC(&pPlayer->Base, out GOCPlayerKinematicParams* out_pGOCPlayerKinematicParams))
        return;

    if (out_pGOCPlayerKinematicParams->IsGrounded)
    {
        // Reset stick deadzone.
        SetAxisDeadzone(pPlayer, 0.25f);

        // Reset initial velocity.
        _isInitialKinematicsCaptured = false;

        return;
    }

    var state = (int)Player.State.GetCurrentStateID<Sonic.StateID>(pPlayer);
    
    /* Block the disallowed states from modifying
       physics until that state has expired. */
    if (!IsStatePairAllowed(state) || _previousDisallowedState == state)
    {
        return;
    }
    else
    {
        _previousDisallowedState = -1;
    }

    if (!_allowedStates.Contains(state) && !_rotationOnlyStates.Contains(state))
    {
        _isInitialKinematicsCaptured = false;
        return;
    }

    if (!_isInitialKinematicsCaptured)
    {
        _initialVelocity = out_pGOCPlayerKinematicParams->Velocity;

        _isInitialKinematicsCaptured = true;
    }

    // Don't run code if the game is paused.
    if (GameManager.GetGameObject("UIPauseMenu") != null)
        return;

    // Don't run code if the player is auto running.
    if (Player.State.GetStatePlugin(pPlayer, "StatePluginAutoRun") != null)
        return;

    var worldInput = out_pGOCPlayerKinematicParams->WorldInput;

    // Don't run code if the stick is neutral.
    if (worldInput == Vector3.Zero)
        return;

    // Don't allow changes whilst not upright.
    if (Vector3.Transform(Vector3.UnitY, out_pGOCPlayerKinematicParams->Rotation).Y < 1.0f)
    {
        _isInitialKinematicsCaptured = false;
        return;
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////
    //                                          START PHYSICS CODE                                           //
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////

    // Decrease stick deadzone to allow more precise air movement.
    SetAxisDeadzone(pPlayer, 0.075f);

    // The combined stick axes to control acceleration speed.
    var axisAcceleration = worldInput.Length();

    var stickRotation = VectorHelpers.LookRotation(Vector3.Normalize(out_pGOCPlayerKinematicParams->WorldInput));

    if (pContext->pBlackboardStatus->IsSideView)
    {
        /* The force to multiply with the current velocity
           based on how far the stick is pushed. */
        var force = MathHelpers.Lerp(0.0f, _accelerationSV, axisAcceleration);

        // The speed of yaw rotation determined by the velocity of the player.
        var rotationForceSV = Math.Abs(MathHelpers.Lerp(_rotationForceMaxSV, _rotationForceMinSV,
            VectorHelpers.GetHorizontalMagnitude(out_pGOCPlayerKinematicParams->Velocity) / 1000.0f));

        out_pGOCPlayerKinematicParams->Velocity += VectorHelpers.GetForward(out_pGOCPlayerKinematicParams->Rotation) * force;
        out_pGOCPlayerKinematicParams->Rotation = Quaternion.Slerp(out_pGOCPlayerKinematicParams->Rotation, stickRotation, rotationForceSV);

        return;
    }

    var forward = VectorHelpers.GetForward(out_pGOCPlayerKinematicParams->Rotation);
    var dot = Vector3.Dot(forward, out_pGOCPlayerKinematicParams->WorldInput);

    // Don't change physics if the player is holding back in the opposite direction.
    if (1.0f - (float)-Math.Min(dot, 0) < _axisDecelerationThreshold)
    {
        _isInitialKinematicsCaptured = false;
        return;
    }

    var acceleration = GetAcceleration(state);
    
    var magnitudeMax = Math.Max(VectorHelpers.GetHorizontalMagnitude(_initialVelocity),
        VectorHelpers.GetVerticalMagnitude(_initialVelocity) / 2.0f);

    var magnitudeMin = acceleration <= 0.0f
        ? magnitudeMax
        : magnitudeMax - (magnitudeMax / acceleration);
    
    var magnitude = MathHelpers.Lerp(magnitudeMin, magnitudeMax, axisAcceleration);

    if (_allowedStates.Contains(state) && !IsDeceleratingOnCollision(pPlayer, out_pGOCPlayerKinematicParams))
        SetVelocityXZ(out_pGOCPlayerKinematicParams, VectorHelpers.GetForward(out_pGOCPlayerKinematicParams->Rotation) * magnitude);

    var targetRotation = VectorHelpers.LookRotation(Vector3.Normalize(out_pGOCPlayerKinematicParams->WorldInput));
    var up = VectorHelpers.GetUp(out_pGOCPlayerKinematicParams->Rotation);
    var vertVelocity = up * Vector3.Dot(up, out_pGOCPlayerKinematicParams->Velocity);
    var horzVelocity = out_pGOCPlayerKinematicParams->Velocity - vertVelocity;
    var rotationForceFV = IsFalling(state) ? _rotationForceFallFV : _rotationForceFV;
    var speed = Math.Max(1.0f, horzVelocity.Length() * rotationForceFV);
    var multiplier = 1.0f / speed;

    out_pGOCPlayerKinematicParams->Rotation =
        VectorHelpers.RotateTowards(out_pGOCPlayerKinematicParams->Rotation,
            targetRotation, 540.0f * multiplier * DeltaTime.GetDeltaTime());
}